﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using mBlog.Core.Extensions;
using mBlog.Data;
using System.Data.Objects;
using mBlog.Core.Models;

namespace mBlog.Core.Services
{
    /// <summary>
    /// The class has all post related operations, including add, update, delete and get posts
    /// from repositories
    /// </summary>
    public class PostService : IPostService
    {
        private mBlogContext blogContext;

        public PostService(mBlogContext blogContext)
        {
            this.blogContext = blogContext;
        }

        public Post First()
        {
            return blogContext.Posts.Include("Comments")
                .OrderByDescending(p => p.PublishedTime)
                .First()
                .MapTo<Post>();

        }

        /// <summary>
        /// Get a post by its id. Exception will be throw if none or more than one post found
        /// </summary>
        /// <param name="id">Id</param>
        /// <returns>Post</returns>
        public Post Single(long id)
        {

            return blogContext.Posts
                .Include("Categories")
                .Include("Tags")
                .Include("Comments")
                .Single(p => p.Id == id)
                .MapTo<Post>();

        }

        /// <summary>
        /// Get a post by its urlName. Exception will be throw if none or more than one post found
        /// </summary>
        /// <param name="urlName">urlName</param>
        /// <returns>Post</returns>
        public Post Single(string urlName)
        {
            return blogContext.Posts
                .Include("Categories")
                .Include("Tags")
                .Include("Comments")
                .Single(p => p.UrlName == urlName)
                .MapTo<Post>();
        }

        /// <summary>
        /// Add a new Post with it's category and tag information
        /// </summary>
        /// <param name="post">The new post</param>
        public void Add(Post post)
        {
            var entity = blogContext.Posts.SingleOrDefault(p => p.Id == post.ID);

            if (entity != null && entity.Id > 0 )
            {
                Update(post);
                return;
            }

            var postEntity = post.MapTo<Data.Entities.Post>();
            blogContext.Posts.Add(postEntity);

            if (post.Categories != null)
            {
                postEntity.Categories = new List<Data.Entities.Category>();

                var categories = from c in post.Categories
                                 join cat in blogContext.Categories on c.UrlName equals cat.UrlName
                                 select cat;

                categories.ToList().ForEach(c => c.Posts.Add(postEntity));

                
                postEntity.Categories.Concat(categories);
            }

            if (post.Tags != null)
            {
                postEntity.Tags = new List<Data.Entities.Tag>();

                var existedTags = from t in post.Tags
                                  join tag in blogContext.Tags on t.UrlName equals tag.UrlName
                                  where t.IsActive
                                  select tag;

                existedTags.ToList().ForEach(t => t.Posts.Add(postEntity));


                var newTags = from t in post.Tags
                              where !existedTags.Any(tag => tag.UrlName == t.UrlName)
                              select new Data.Entities.Tag
                              {
                                  Name = t.Name,
                                  UrlName = t.UrlName,
                                  IsActive = true,
                                  AddedTime = DateTime.Now,
                                  Posts = new List<Data.Entities.Post>()
                              };

                newTags.ToList().ForEach(t => t.Posts.Add(postEntity));

                postEntity.Tags.Concat(existedTags).Concat(newTags);
            }

            blogContext.SaveChanges();
            
        }

        /// <summary>
        /// Add tags to a Post. New tags will be added to tag library.
        /// </summary>
        /// <param name="postId">Id of the Post</param>
        /// <param name="tags">A list of tags</param>
        public void AddTags(long postId, IList<string> tags)
        {
            var entity = blogContext.Posts.SingleOrDefault(p => p.Id == postId);
            if (entity == null)
                return;

            if (entity.Tags == null)
                entity.Tags = new List<Data.Entities.Tag>();

            //tag already exists
            var existTags = from t in tags
                            join tag in blogContext.Tags on t equals tag.Name
                            where tag.IsActive
                            select tag;

            existTags.ToList().ForEach(t =>
                    {
                        if (!entity.Tags.Contains(t))
                        {
                            entity.Tags.Add(t);
                            t.Posts.Add(entity);
                        }
                    }
                );

            //new tag
            var newTags = from t in tags
                          where !blogContext.Tags.Any(tag => tag.Name == t)
                          select t;

            foreach (var newTag in newTags)
            {
                Data.Entities.Tag tag = new Data.Entities.Tag()
                {
                    Name = newTag,
                    UrlName = newTag,
                    IsActive = true,
                    AddedTime = DateTime.Now,
                    Posts = new List<Data.Entities.Post>()
                };

                tag.Posts.Add(entity);
                entity.Tags.Add(tag);
            }

            blogContext.SaveChanges();
        }

        /// <summary>
        /// Remove a tag from a post.
        ///
        /// </summary>
        /// <param name="postId">Id of the Post</param>
        /// <param name="tagId">Id of the Tag</param>
        public void DeleteTag(long postId, long tagId)
        {
            var post = blogContext.Posts.Single(p => p.Id == postId);
            if (post != null)
            {
                var tag = blogContext.Tags.Single(t => t.Id == tagId);
                post.Tags.Remove(tag);

                blogContext.SaveChanges();
            }
        }

        public IList<Post> GetByCategory(string catUrlname, int? page, int itemsPerPage)
        {
            var cat = blogContext.Categories.Include("Posts")
                .Single(c => c.UrlName == catUrlname);
            var posts = cat.Posts.AsQueryable()
                        .AtPage<Data.Entities.Post>(page, itemsPerPage);
            return posts.MapTo<Post>();
        }

        public IList<Post> GetByTag(string tagUrlName, int? page, int itemsPerPage)
        {
            var tag = blogContext.Tags.Include("Posts").Single(t => t.UrlName == tagUrlName);
            var posts = tag.Posts.AsQueryable().AtPage<Data.Entities.Post>(page, itemsPerPage);
            return posts.MapTo<Post>();
        }

        public IList<Post> GetByDate(short year, short month, int? page, int itemsPerPage)
        {
            var posts = blogContext.Posts.Where(p => p.PostedTime.Year == year && p.PostedTime.Month == month)
                            .AtPage<Data.Entities.Post>(page, itemsPerPage);

            return posts.MapTo<Post>();
        }

        /// <summary>
        /// Get published posts according to page number and items to take
        /// </summary>
        /// <param name="page">page number</param>
        /// <param name="itemsToTake">items to take</param>
        /// <returns>A list of posts</returns>
        public IList<Post> Get(int? page, int itemsToTake)
        {
            var posts = blogContext.Posts.Where(p => p.IsPublished).OrderByDescending(p => p.PostedTime)
                        .AtPage<Data.Entities.Post>(page, itemsToTake);

            return posts.MapTo<Post>();
        }

        /// <summary>
        /// Get posts count, by default unpublished posts are not counted
        /// </summary>
        /// <param name="includeUnpublished">if true, count on unpublished posts</param>
        /// <returns>total count of posts</returns>
        public int Count(bool? includeUnpublished = null)
        {
            var posts = !includeUnpublished.HasValue ? blogContext.Posts.Where(p => p.IsPublished)
                            : blogContext.Posts.AsQueryable();

            return posts.Count();
        }

        /// <summary>
        /// Delete a post by it's id
        /// </summary>
        /// <param name="postId">Post ID</param>
        public void Delete(long postId)
        {
            var entity = blogContext.Posts.Single(p => p.Id == postId);
            blogContext.Posts.Remove(entity);

            blogContext.SaveChanges();
        }


        public void Update(Post post)
        {
            var entity = blogContext.Posts.SingleOrDefault(p => p.Id == post.ID);
            if (entity == null) return;

            post.MapPropertiesToInstance(entity);

            var newCategories = from nc in post.Categories
                                join c in blogContext.Categories on nc.UrlName equals c.UrlName
                                where c.IsActive
                                select c;

            var existedCategories = newCategories.Intersect(entity.Categories).ToList();

            var categoriesToRemove = entity.Categories.Except(existedCategories).ToList();

            foreach (var category in categoriesToRemove)
            {
                category.Posts.Remove(entity);
                entity.Categories.Remove(category);
            }

            var categoriesToAdd = newCategories.Except(existedCategories);

            foreach (var category in categoriesToAdd)
            {
                category.Posts.Add(entity);
                entity.Categories.Add(category);
            }

            blogContext.SaveChanges();

        }


        //public void UpdateCategory(long postId, IList<Category> newCategories)
        //{
        //    var entity = blogContext.Posts.SingleOrDefault(p => p.ID == postId);
        //    if (entity == null)
        //        return;

        //    var categoriesToAdd = from nc in newCategories
        //                          join c in blogContext.Categories on nc.UrlName equals c.UrlName
        //                          where c.IsActive
        //                          select c;
            

        //    blogContext.SaveChanges();

        //}


        public void AddComment(long postId, Comment comment)
        {
            var postEntity = blogContext.Posts.SingleOrDefault(p => p.Id == postId);
            if (postEntity == null)
                return;

            var cmtEntity = new Data.Entities.Comment();
            comment.MapPropertiesToInstance(cmtEntity);

            cmtEntity.Post = postEntity;
            postEntity.Comments.Add(cmtEntity);

            blogContext.SaveChanges();
        }

        public void DeleteComment(long commentId)
        {
            var cmtEntity = blogContext.Comments.SingleOrDefault(p => p.Id == commentId);
            if (cmtEntity == null)
                return;

            blogContext.Comments.Remove(cmtEntity);
            blogContext.SaveChanges();
        }


        public IList<Comment> GetRecentComment(int itemsToTake)
        {
            return blogContext.Comments.OrderByDescending(c => c.CommentTime).Take(itemsToTake).MapTo<Comment>();
        }
    }
}
